﻿using System;
using Brjnk.Common;
using Brjnk.GpxExplorer.Data;
using Brjnk.GpxExplorer.ViewModel;
using Brjnk.MVVM;

namespace Brjnk.GpxExplorer.Computing
{
    internal class TrackCalculationHelper
    {
        private int number = -1;

        private int numberInSegment = -1;

        private int segmentNumber = -1;

        private TrackSegmentVM parentSegment;

        private TrackVM track;

        private CollectionWrapper<TrackSegment, TrackSegmentVM> segments;

        private TrackCalculationContext context = new TrackCalculationContext();

        public TrackCalculationHelper(TrackVM track)
        {
            this.track = track;
            this.segments = track.Segments;
            var context = new TrackCalculationContext();
        }

        public void Calculate()
        {
            //Určit TrackPointFlag a vypočítat vzdálenost
            TrackPointVM previous = null;

            for (int m = 0; m < segments.Count; m++)
            {
                var segment = segments[m];
                //   if (segment.Points.Count == 0) { segments.RemoveAt(m); m--; continue; } //odebrat segment, pokud nemá žádné body

                bool firstSegmentInTrack = m == 0;
                bool lastSegmentInTrack = m == segments.Count - 1;

                for (int n = 0; n < segment.Points.Count; n++)
                {
                    var point = segment.Points[n];
                    point.Flag = GetFlag(firstSegmentInTrack, lastSegmentInTrack, n == 0, n == segment.Points.Count - 1);
                    CalculateDistance(point, previous, n == 0);
                    previous = point;
                }
            }

            //Dopočítat ostatní hodnoty (kromě vzdálenosti, která již je vypočítaná)
            foreach (var segment in segments)
            {
                StartNewSegment(segment);
                BeginDetermineElevationExtremes();
                foreach (var point in segment.Points)
                {
                    ProcessPoint(point);
                }
                EndDetermineElevationExtremes();
            }

            //vypočítat souhrné hodnoty pro celou stopu
            if (context.Last != null)
            {
                Distance = context.Last.Distance.Value;
                DistanceJoined = context.Last.DistanceJoined.Value;
                var lastWithTime = context.LastWithTime;
                var lastWithHeight = context.LastWithElevation;
                if (lastWithTime != null)
                {
                    if (lastWithTime.Duration.HasValue) Duration = lastWithTime.Duration.Value;
                    if (lastWithTime.DurationJoined.HasValue) DurationJoined = lastWithTime.DurationJoined.Value;
                }
                else { Duration = null; DurationJoined = null; }
                if (lastWithHeight != null)
                {
                    if (lastWithHeight.Rised.HasValue) Rised = lastWithHeight.Rised.Value;
                    if (lastWithHeight.RisedJoined.HasValue) RisedJoined = lastWithHeight.RisedJoined.Value;
                    if (lastWithHeight.Fallen.HasValue) Fallen = lastWithHeight.Fallen.Value;
                    if (lastWithHeight.FallenJoined.HasValue) FallenJoined = lastWithHeight.FallenJoined.Value;
                }
                else { Rised = null; RisedJoined = null; Fallen = null; FallenJoined = null; }
                if (avgSpeedDistance != 0 || avgSpeedTime != 0) AvgSpeed = (Speed)avgSpeedDistance / avgSpeedTime;
                if (avgSpeedDistanceJoined != 0 || avgSpeedTimeJoined != 0) AvgSpeedJoined = (Speed)avgSpeedDistanceJoined / avgSpeedTimeJoined;
            }

            //dopočítá hodnoty pro poslední segment
            StartNewSegment(null);
        }

        /// <param name="firstSegmentInTrack">True, pokud se jedná o první segment ve stopě</param>
        /// <param name="lastSegmentInTrack">True, pokud se jedná o poslední segment ve stopě</param>
        /// <param name="firstInSegment">True, pokud se jedná o první bod v segmentu</param>
        /// <param name="lastInSegment">True, pokud se jedná o poslední bod v segmentu</param>
        /// <returns></returns>
        private TrackPointFlag GetFlag(bool firstSegmentInTrack, bool lastSegmentInTrack, bool firstInSegment, bool lastInSegment)
        {
            if (firstInSegment)
            {
                if (firstSegmentInTrack) return TrackPointFlag.FirstInTrack;
                return TrackPointFlag.FirstInSegment;
            }

            if (lastSegmentInTrack)
            {
                if (lastInSegment)
                {
                    if (lastSegmentInTrack) return TrackPointFlag.LastInTrack;
                    return TrackPointFlag.LastInSegment;
                }
            }
            return TrackPointFlag.None;
        }

        private void StartNewSegment(TrackSegmentVM segment)
        {
            if (context.FirstPointInSegment != null)
            {
                parentSegment.StartTime = context.FirstPointInSegment.Source.Time;
                parentSegment.EndTime = context.Last.Source.Time;
                parentSegment.Distance = context.Last.Distance.Value - context.FirstPointInSegment.Distance.Value;
                parentSegment.Num = segmentNumber;
            }

            //Funkce je volána s null argumentem po skončení výpočtu, aby se nastavily hodnoty pro poslední segment (kód výše)
            if (segment == null) return;
            segmentNumber++;
            numberInSegment = -1;

            context.StartNewSegment();

            parentSegment = segment;
        }

        public void ProcessPoint(TrackPointVM point)
        {
            number++;
            numberInSegment++;

            point.Number = number;
            point.SegmentNumber = segmentNumber;
            point.NumberInSegment = numberInSegment;

            Calculate(point);

            context.ProcessPoint(point);
        }

        private TimeSpan lastDuration;

        private Distance lastRised;

        private Distance lastFallen;

        ////slouží pro výpočet průměrné rychlosti, hodnoty jsou nastaveny v metodě Calculate
        //private double avgSpeedDistance, avgSpeedTime;

        //private double avgSpeedDistanceJoined, avgSpeedTimeJoined;

        private void Calculate(TrackPointVM point)
        {
            CalculateTime(point);

            CalculateElevation(point);

            DetermineElevationExtremes(point);

            SpeedCalculator.Calculate(context.LastWithTime, context.IsLastWithTimeInSegment, point);

            VerticalSpeedCalculator.Calculate(context.LastWithTimeElevation, context.IsLastWithTimeElevationInSegment, point);

            AccelerationCalculator.Calculate(context.LastWithTime, context.IsLastWithTimeInSegment, point);

            SlopeCalculator.Calculate(context.LastWithElevation, context.IsLastWithElevationInSegment, point);

            //  ApproximateUnavailableValues(point);
        }

        private TrackPointVM eleMax, eleMin;
        private bool? lastMax; //true, pokud byl poslední označený extrém maximum
        private const double elevationExtremeLimit = 10.0;

        private void DetermineElevationExtremes(TrackPointVM point)
        {
            point.ElevationLocalExtreme = ElevationExtreme.None;
            if (!context.IsLastInSegment) { eleMax = null; eleMin = null; }

            if (point.Source.Elevation.HasValue)
            {
                Distance elevation = point.Source.Elevation.Value;
                bool lastMaxExceed = false; //true, pokud naposled byla překročena hodnota eleMax

                if (eleMax == null || elevation > eleMax.Source.Elevation) { eleMax = point; lastMaxExceed = true; };
                if (eleMin == null || elevation < eleMin.Source.Elevation) { eleMin = point; lastMaxExceed = false; }

                if (!lastMax.HasValue)
                {
                    if (Math.Abs(eleMax.Source.Elevation.Value - eleMin.Source.Elevation.Value) >= elevationExtremeLimit)
                    {
                        if (lastMaxExceed) SetLocalEleMin(point);
                        else SetLocalEleMax(point);
                    }
                }
                else if (lastMax.Value)
                {
                    if ((elevation - eleMin.Source.Elevation.Value) > elevationExtremeLimit) SetLocalEleMin(point);
                }
                else
                {
                    if ((eleMax.Source.Elevation.Value - elevation) > elevationExtremeLimit) SetLocalEleMax(point);
                }
            }
        }

        private void BeginDetermineElevationExtremes()
        {
            eleMax = null;
            eleMin = null;
            lastMax = null;
        }

        private void EndDetermineElevationExtremes()
        {
            //dopočítat extrém na konci stopy
            if (lastMax.HasValue && eleMax != null && eleMin != null)
            {
                if (eleMax.Source.Elevation - eleMin.Source.Elevation > elevationExtremeLimit)
                {
                    if (lastMax.Value) SetLocalEleMin(null);
                    else SetLocalEleMax(null);
                }
            }
        }

        private void SetLocalEleMax(TrackPointVM current)
        {
            eleMax.ElevationLocalExtreme = ElevationExtreme.Maximum;
            eleMin = current;
            lastMax = true;
        }

        private void SetLocalEleMin(TrackPointVM current)
        {
            eleMin.ElevationLocalExtreme = ElevationExtreme.Minimum;
            eleMax = current;
            lastMax = false;
        }

        private void ApproximateUnavailableValues(TrackPointVM point)
        {
            if (point.Source.Elevation.HasValue && context.LastWithElevation != null)
                ApproximateElevation(context.LastWithElevation, context.IsLastWithElevationInSegment, point);

            if (point.Source.Time.HasValue && context.LastWithTime != null)
                ApproximateTime(context.LastWithTime, context.IsLastWithTimeInSegment, point);
        }

        private void ApproximateTime(TrackPointVM last, bool isInSegment, TrackPointVM point)
        {
            double distance = point.Distance.GetDoubleValue() - last.Distance.GetDoubleValue();
            double distanceJoined = point.DistanceJoined.GetDoubleValue() - last.DistanceJoined.GetDoubleValue();

            double durationDeltaJoined = point.DurationJoined.GetDoubleValue() - last.DurationJoined.GetDoubleValue();
            double durationDelta = point.Duration.GetDoubleValue() - last.Duration.GetDoubleValue();

            for (int s = last.SegmentNumber; s < point.SegmentNumber; s++)
            {
                var segment = segments[s];
                for (int p = last.NumberInSegment; p <= segment.Points.Count; p++)
                {
                    if (s == last.SegmentNumber && p == last.NumberInSegment) continue;
                    if (s == point.SegmentNumber && p == point.NumberInSegment) break;
                    var current = segment.Points[p];

                    double nJoined = (current.DistanceJoined.GetDoubleValue() - last.DistanceJoined.GetDoubleValue()) / distanceJoined;

                    if (isInSegment)
                    {
                        double n = (current.Distance.GetDoubleValue() - last.Distance.GetDoubleValue()) / distance;
                        current.Duration = new TrackPointAttributeValue<TimeSpan>(TimeSpan.FromSeconds(last.Duration.GetDoubleValue() + durationDelta * n), true);
                    }
                    current.DurationJoined = new TrackPointAttributeValue<TimeSpan>(TimeSpan.FromSeconds(last.DurationJoined.GetDoubleValue() + durationDeltaJoined * nJoined), true);
                    current.Time = new TrackPointAttributeValue<DateTime>(new DateTime((long)(last.Time.GetDoubleValue() + durationDeltaJoined * nJoined) * 10000000), true);
                }
            }
        }

        private void ApproximateElevation(TrackPointVM last, bool isInSegment, TrackPointVM point)
        {
            double distance = point.Distance.GetDoubleValue() - last.Distance.GetDoubleValue();
            double distanceJoined = point.DistanceJoined.GetDoubleValue() - last.DistanceJoined.GetDoubleValue();

            double fallenDeltaJoined = point.FallenJoined.GetDoubleValue() - last.FallenJoined.GetDoubleValue();
            double fallenDelta = point.Fallen.GetDoubleValue() - last.Fallen.GetDoubleValue();
            double risedDeltaJoined = point.RisedJoined.GetDoubleValue() - last.RisedJoined.GetDoubleValue();
            double risedDelta = point.Rised.GetDoubleValue() - last.Rised.GetDoubleValue();
            double elevationDelta = point.Elevation.GetDoubleValue() - last.Elevation.GetDoubleValue();

            foreach (var current in track.GetPoints(last.SegmentNumber, last.NumberInSegment, point.SegmentNumber, point.NumberInSegment, false, false))
            {
                double nJoined = (current.DistanceJoined.GetDoubleValue() - last.DistanceJoined.GetDoubleValue()) / distanceJoined;

                if (isInSegment)
                {
                    double n = (current.Distance.GetDoubleValue() - last.Distance.GetDoubleValue()) / distance;
                    current.Fallen = new TrackPointAttributeValue<Distance>(last.Fallen.GetDoubleValue() + fallenDelta * n, true);
                    current.Rised = new TrackPointAttributeValue<Distance>(last.Rised.GetDoubleValue() + risedDelta * n, true);
                }
                current.FallenJoined = new TrackPointAttributeValue<Distance>(last.FallenJoined.GetDoubleValue() + fallenDeltaJoined * nJoined, true);
                current.RisedJoined = new TrackPointAttributeValue<Distance>(last.RisedJoined.GetDoubleValue() + risedDeltaJoined * nJoined, true);
                current.Elevation = new TrackPointAttributeValue<Distance>(last.Elevation.GetDoubleValue() + elevationDelta * nJoined, true);
            }
        }

        private void CalculateElevation(TrackPointVM point)
        {
            if (point.Source.Elevation.HasValue)
                point.Elevation = new TrackPointAttributeValue<Common.Distance>(point.Source.Elevation.Value.GetValue());
            else point.Elevation = TrackPointAttributeValue<Distance>.CreateUnavailable();

            var lastWithHeight = context.LastWithElevation;

            //výpočet hodnot nastoupáno (rised) a naklesáno (fallen)
            if (lastWithHeight != null && point.Source.Elevation.HasValue)
            {
                var delta = point.Source.Elevation.Value - lastWithHeight.Source.Elevation.Value;
                point.RisedJoined = (lastWithHeight.RisedJoined.Value + Math.Max(0, delta));
                point.FallenJoined = lastWithHeight.FallenJoined.Value + Math.Min(0, delta);
                point.ElevationDeltaJoined = (Distance)delta;
                if (context.IsLastWithElevationInSegment)
                {
                    lastRised = lastWithHeight.Rised.Value + Math.Max(0, delta);
                    lastFallen = lastWithHeight.Fallen.Value + Math.Min(0, delta);
                    point.Rised = lastRised;
                    point.Fallen = lastFallen;
                    point.HasElevationDelta = true;
                }
                else
                {
                    point.Rised = lastRised;
                    point.Fallen = lastFallen;
                    point.HasElevationDelta = false;
                }
            }
            else if (point.Source.Elevation.HasValue)
            {
                point.RisedJoined = (Distance)0;
                point.FallenJoined = (Distance)0;
                point.Fallen = (Distance)0;
                point.Rised = (Distance)0;
                point.ElevationDeltaJoined = TrackPointAttributeValue<Distance>.CreateUnset();
                point.HasElevationDelta = false;
            }
            else
            {
                point.RisedJoined = TrackPointAttributeValue<Distance>.CreateUnavailable();
                point.FallenJoined = TrackPointAttributeValue<Distance>.CreateUnavailable();
                point.Fallen = TrackPointAttributeValue<Distance>.CreateUnavailable();
                point.Rised = TrackPointAttributeValue<Distance>.CreateUnavailable();
                point.ElevationDeltaJoined = TrackPointAttributeValue<Distance>.CreateUnavailable();
                point.HasElevationDelta = true;
            }
        }

        //slouží pro výpočet průměrné rychlosti, hodnoty jsou nastaveny v metodě Calculate
        private double avgSpeedDistance, avgSpeedTime;

        private double avgSpeedDistanceJoined, avgSpeedTimeJoined;

        private void CalculateTime(TrackPointVM point)
        {
            var lastWithTime = context.LastWithTime;
            if (point.Source.Time.HasValue) point.Time = point.Source.Time.Value;
            else point.Time = TrackPointAttributeValue<DateTime>.CreateUnavailable();

            //výpočet času (duration, durationJoined, durationDelta)
            if (lastWithTime != null && point.Source.Time.HasValue)
            {
                var delta = point.Source.Time.Value - lastWithTime.Source.Time.Value;
                point.DurationDeltaJoined = delta;
                point.DurationJoined = lastWithTime.DurationJoined.Value + delta;
                if (context.IsLastWithTimeInSegment)
                {
                    lastDuration = lastWithTime.Duration.Value + delta;
                    point.Duration = lastDuration;
                    point.HasDurationDelta = true;

                    avgSpeedTime += delta.TotalSeconds;
                    avgSpeedDistance += (double)point.Distance.Value - (double)lastWithTime.Distance.Value;
                    avgSpeedTimeJoined += delta.TotalSeconds;
                    avgSpeedDistanceJoined += (double)point.Distance.Value - (double)lastWithTime.Distance.Value;
                }
                else
                {
                    point.Duration = lastDuration;
                    point.HasDurationDelta = false;

                    avgSpeedTimeJoined += delta.TotalSeconds;
                    avgSpeedDistanceJoined += (double)point.Distance.Value - (double)lastWithTime.Distance.Value;
                }
            }
            else if (point.Source.Time.HasValue)
            {
                point.Duration = TimeSpan.Zero;
                point.DurationJoined = TimeSpan.Zero;
                point.DurationDeltaJoined = TrackPointAttributeValue<TimeSpan>.CreateUnset();
                point.HasDurationDelta = false;
            }
            else
            {
                point.Duration = TrackPointAttributeValue<TimeSpan>.CreateUnavailable();
                point.DurationJoined = TrackPointAttributeValue<TimeSpan>.CreateUnavailable();
                point.DurationDeltaJoined = TrackPointAttributeValue<TimeSpan>.CreateUnavailable();
                point.HasDurationDelta = true;
            }
        }

        private void CalculateDistance(TrackPointVM point, TrackPointVM previous, bool isFirstInSegment)
        {
            //výpočet dráhy (distance, distanceJoined, distanceDelta)
            if (previous != null)
            {
                var delta = previous.Source.Position - point.Source.Position;
                point.DistanceDeltaJoined = new TrackPointAttributeValue<Distance>(delta);
                point.DistanceJoined = (Distance)(previous.DistanceJoined.Value + delta);
                point.AzimuthJoined = (Azimuth)Azimuth.GetAzimuth(previous.PositionI, point.PositionI);
                if (!isFirstInSegment)
                {
                    point.Distance = (Distance)(previous.Distance.Value + delta);
                }
                else { point.Distance = previous.Distance; }
            }
            else
            {
                point.DistanceJoined = (Distance)0;
                point.Distance = (Distance)0;
                point.DistanceDeltaJoined = TrackPointAttributeValue<Distance>.CreateUnset();
                point.AzimuthJoined = TrackPointAttributeValue<Azimuth>.CreateUnset();
            }
        }

        public double DistanceJoined { get; private set; }

        public double Distance { get; private set; }

        public TimeSpan? DurationJoined { get; private set; }

        public TimeSpan? Duration { get; private set; }

        public double? Rised { get; private set; }

        public double? RisedJoined { get; private set; }

        public double? Fallen { get; private set; }

        public double? FallenJoined { get; private set; }

        public double? AvgSpeed { get; private set; }

        public double? AvgSpeedJoined { get; private set; }
    }
}